Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

Mod: Weird Problem with Copper Pieces {cp} for One Character

1783442473

Edited 1783442874
One of the first scripts I worked on the past spring was a Mod chat that shows a player what their coins are and to be able to change their value. I ask the guys to just stick with with coins from the sheet and not use the Total Coins. One character is acting weirdly but only for copper. There appears to be some kind of oddness associated with the value cp on the sheet.&nbsp; I get take the values for cp and other coins in the procedure that comes before the adding/subtracting a numnber by using:&nbsp; let carCP = parseInt(getAttrByName(charID,"cp"),10) || 0; let carSP = parseInt(getAttrByName(charID,"sp"),10) || 0; let carEP = parseInt(getAttrByName(charID,"ep"),10) || 0; let carGP = parseInt(getAttrByName(charID,"gp"),10) || 0; let carPP = parseInt(getAttrByName(charID,"pp"),10) || 0; Here is the result for one player for whom I am not having the subject issue. Clicking on one of the coin buttons brings allows you to enter a negative or positive number to alter your total for that coin. The process involves using !modattr from ChatSetAttr to change the value of the coin selected on the character sheet.&nbsp; Here I will add 10 CP for Fadder Dave: After Submit is clicked !modattr does its thing and changes the cp value on the sheet to 10 + 0&nbsp;and the chat window is relaunched. I have added a couple of sendChats for debugging.&nbsp; Note how the Setting Attributes &nbsp;indicates the new value added to Fadder Dave's copper. He started off with 0 CP, but the process works fine with everyone else, except the problem character, Nigel Krenov, and only with CP. Nigel Krenov&nbsp;worked fine until now. Here is the above process with the naughty character. Again, this happens only for copper pieces, the other coins all work fine for him: Now look at the highlighted areas of the debugging: That value for 1070 should jolly well be 80! &nbsp;The current value for cp is determined in the first code shown at the start of this post. The debugging is as follows - I'll include the full code to changing the coin amounts afterwards: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;let attrs = findObjs({ type: "attribute", characterid: CoinCharId, name: "cp" }); attrs.forEach(a =&gt; { sendChat("CP Debug", "attr id: " + a.id + "&lt;br&gt;name: " + a.get("name") + "&lt;br&gt;current: " + a.get("current") ); }); The character sheet shows 80 CP, both before and after (and it stays 80, no matter what): Somehow,&nbsp; this has identified that for Nigel the cp value from the sheet gets confused. I may end up deleting this character and re-adding him again. But any idea how this might have happened? I did have problems with my game being very slow to load and mods going dormant when the game was left idle for a few minutes. I did the following: <a href="https://app.roll20.net/forum/post/12765979/is-there-any-way-to-make-mods-respond-more-quickly/?pageforid=12779824#post-12779824" rel="nofollow">https://app.roll20.net/forum/post/12765979/is-there-any-way-to-make-mods-respond-more-quickly/?pageforid=12779824#post-12779824</a> It could well be whatever the issue was with my original game may have taken its toll on Making Plans for Nigel.&nbsp; FWIW, the whole code for the process is: on("chat:message", function(msg) { if (msg.type !== "api") return; let chatcommand = msg.content.trim().toLowerCase(); if (!chatcommand.startsWith("!changecoins")) return; let args = msg.content.trim().split(/\s*--/); let NewCoins = parseInt(args[1], 10) || 0 //Number to add/subtract let CoinCharId = (args[2] || "").trim(); //CharID let CoinP = args[3].trim().toLowerCase(); //for example PP let CoinName = (args[4] || "").trim(); //eg Platinum Pieces let CoinCharName = (args[5] || "").trim(); //character name let Coins = parseInt(args[6], 10) || 0; //current number of coins let commandvalue = ""; if (isNaN(Coins)) { sendChat("Sorry, but...", "&lt;div style='color:red;'&gt;Added number of " + CoinP + " must be a positive number or possibly zero!"&lt;/div&gt;"); return; } else if (Coins + NewCoins &lt; -1) { sendChat("Sorry, but...", "&lt;div style='color:red;'&gt;" + NewCoins + " is more than the current number of " + CoinP + " " + "that " + CoinCharName + " currently has. &lt;/div&gt;"); return; } //log(CoinP.toLowerCase() + " new coins " + Coins + " Current Coins " + NewCoins); //let FinalCoins = NewCoins; //sendChat("Operation", CoinCharName + " current " + CoinP + ": " + Coins + " to a 'FinalCoins' Total: " + NewCoins); sendChat(CoinCharName, "CP on Sheet: " + getAttrByName(CoinCharId,"cp") + "; Added " + NewCoins); let attrs = findObjs({ type: "attribute", characterid: CoinCharId, name: "cp" }); attrs.forEach(a =&gt; { sendChat("CP Debug", "attr id: " + a.id + "&lt;br&gt;name: " + a.get("name") + "&lt;br&gt;current: " + a.get("current") ); }); //using getAttrByName(CoinCharId,'cp') where 'CoinCharId' is Character ID: " + getAttrByName(CoinCharId,"cp")); if (CoinP.toLowerCase() === "cp") { commandvalue = `!modattr --charid ${CoinCharId} --cp|${NewCoins}`; } else if (CoinP.toLowerCase() === "sp") { commandvalue = `!modattr --charid ${CoinCharId} --sp|${NewCoins}`; } else if (CoinP.toLowerCase() === "ep") { commandvalue = `!modattr --charid ${CoinCharId} --ep|${NewCoins}`; } else if (CoinP.toLowerCase() === "gp") { commandvalue = `!modattr --charid ${CoinCharId} --gp|${NewCoins}`; } else if (CoinP.toLowerCase() === "pp") { commandvalue = `!modattr --charid ${CoinCharId} --pp|${NewCoins}`; } else { sendChat( "Error", `&lt;div style='color:red;'&gt;Unknown coin type: ${CoinP}&lt;/div&gt;` ); return; } /*sendChat("Debug", "CoinCharName: " + CoinCharName + "&lt;br&gt;CoinCharId: " + CoinCharId + "&lt;br&gt;CoinP: " + CoinP + "&lt;br&gt;NewCoins: " + NewCoins + "&lt;br&gt;Command: " + commandvalue );*/ sendChat ("Change " + CoinP, commandvalue); sendChat("My Coins, Altered", "!forselected !mycoins"); });
I cleaned up the coin-changing script so it handles bad input more reliably and gives clearer debugging when a coin value acts strangely. The biggest change is that it now checks whether a character has more than one attribute named cp sp etc.&nbsp;since Nigel’s issue looks like Roll20 may have duplicate cp&nbsp;attributes, where one value is shown on the sheet while another is being changed by ChatSetAttr. I also added a debug option that shows the character name, the value Roll20 sees from getAttrByName , and every matching coin attribute object attached to that character. A repair option was added as well, which can force all matching coin attributes for a character to the same value without deleting anything. The script also waits briefly before reopening the coin menu after ChatSetAttr runs, so the menu is less likely to reload before the value has actually updated. I also improved number validation so bad input is rejected instead of quietly becoming zero, corrected the logic so a character cannot be reduced below 0 coins. // Coin Change Script // Works with ChatSetAttr's !modattr command. // // Main command format expected: // !changecoins --NUMBER_TO_ADD_OR_SUBTRACT --CHAR_ID --COIN_TYPE --COIN_NAME --CHARACTER_NAME --CURRENT_COINS // // Example: // !changecoins --10 ---Nabc123xyz --cp --Copper Pieces --Nigel Krenov --80 // // Repair command: // !repaircoinattr --charid CHARACTER_ID --coin cp --value 80 on("chat:message", function(msg) { &nbsp; &nbsp; if (msg.type !== "api") return; &nbsp; &nbsp; let rawContent = msg.content.trim(); &nbsp; &nbsp; let lowerContent = rawContent.toLowerCase(); &nbsp; &nbsp; if (lowerContent.startsWith("!changecoins")) { &nbsp; &nbsp; &nbsp; &nbsp; handleChangeCoins(msg); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (lowerContent.startsWith("!debugcoinattr")) { &nbsp; &nbsp; &nbsp; &nbsp; handleDebugCoinAttr(msg); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (lowerContent.startsWith("!repaircoinattr")) { &nbsp; &nbsp; &nbsp; &nbsp; handleRepairCoinAttr(msg); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } }); function handleChangeCoins(msg) { &nbsp; &nbsp; let args = msg.content.trim().split(/\s*--/); &nbsp; &nbsp; let NewCoinsRaw = args[1]; &nbsp; &nbsp; let CoinCharId = (args[2] || "").trim(); &nbsp; &nbsp; let CoinP = (args[3] || "").trim().toLowerCase(); &nbsp; &nbsp; let CoinName = (args[4] || "").trim(); &nbsp; &nbsp; let CoinCharName = (args[5] || "").trim(); &nbsp; &nbsp; let CoinsRaw = args[6]; &nbsp; &nbsp; let NewCoins = parseInt(NewCoinsRaw, 10); &nbsp; &nbsp; let Coins = parseInt(CoinsRaw, 10); &nbsp; &nbsp; if (!CoinCharId) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", "&lt;div style='color:red;'&gt;No character ID was supplied.&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (!CoinP) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", "&lt;div style='color:red;'&gt;No coin type was supplied.&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (!isValidCoinType(CoinP)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", "&lt;div style='color:red;'&gt;Unknown coin type: " + escapeHtml(CoinP) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (isNaN(NewCoins)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Sorry, but...", "&lt;div style='color:red;'&gt;The coin change must be a number.&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (isNaN(Coins)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Sorry, but...", "&lt;div style='color:red;'&gt;The current number of " + escapeHtml(CoinP.toUpperCase()) + " could not be read correctly.&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (Coins + NewCoins &lt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Sorry, but...", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='color:red;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(String(NewCoins)) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " would reduce " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(CoinCharName) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "'s " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(CoinP.toUpperCase()) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " below 0.&lt;/div&gt;" &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let character = getObj("character", CoinCharId); &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", "&lt;div style='color:red;'&gt;Could not find character with ID: " + escapeHtml(CoinCharId) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let sheetValue = getAttrByName(CoinCharId, CoinP, "current"); &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; characterid: CoinCharId, &nbsp; &nbsp; &nbsp; &nbsp; name: CoinP &nbsp; &nbsp; }); &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(CoinP.toUpperCase()) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Sheet value from getAttrByName:&lt;/b&gt; [" + escapeHtml(String(sheetValue)) + "]&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Change requested:&lt;/b&gt; " + escapeHtml(String(NewCoins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Current coins passed into command:&lt;/b&gt; " + escapeHtml(String(Coins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Expected result:&lt;/b&gt; " + escapeHtml(String(Coins + NewCoins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Attribute objects found:&lt;/b&gt; " + attrs.length + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;/div&gt;" &nbsp; &nbsp; ); &nbsp; &nbsp; if (attrs.length !== 1) { &nbsp; &nbsp; &nbsp; &nbsp; let output = ""; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;div style='border:1px solid #c00;background:#fff;padding:5px;'&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b style='color:#c00;'&gt;Warning: Found " + attrs.length + " attribute objects named " + escapeHtml(CoinP) + ".&lt;/b&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "This can cause Roll20 or ChatSetAttr to update one value while the sheet displays another.&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(function(a, i) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;" + escapeHtml(CoinP.toUpperCase()) + " attribute " + (i + 1) + "&lt;/b&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "attr id: " + escapeHtml(a.id) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "current: [" + escapeHtml(String(a.get("current"))) + "]&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "max: [" + escapeHtml(String(a.get("max"))) + "]&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;/div&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", output); &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(function(a) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Attribute object being targeted by name:&lt;/b&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "attr id: " + escapeHtml(a.id) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "name: " + escapeHtml(a.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "current: [" + escapeHtml(String(a.get("current"))) + "]&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "max: [" + escapeHtml(String(a.get("max"))) + "]" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;/div&gt;" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; } &nbsp; &nbsp; let commandvalue = ""; &nbsp; &nbsp; if (CoinP === "cp") { &nbsp; &nbsp; &nbsp; &nbsp; commandvalue = "!modattr --charid " + CoinCharId + " --cp|" + NewCoins; &nbsp; &nbsp; } else if (CoinP === "sp") { &nbsp; &nbsp; &nbsp; &nbsp; commandvalue = "!modattr --charid " + CoinCharId + " --sp|" + NewCoins; &nbsp; &nbsp; } else if (CoinP === "ep") { &nbsp; &nbsp; &nbsp; &nbsp; commandvalue = "!modattr --charid " + CoinCharId + " --ep|" + NewCoins; &nbsp; &nbsp; } else if (CoinP === "gp") { &nbsp; &nbsp; &nbsp; &nbsp; commandvalue = "!modattr --charid " + CoinCharId + " --gp|" + NewCoins; &nbsp; &nbsp; } else if (CoinP === "pp") { &nbsp; &nbsp; &nbsp; &nbsp; commandvalue = "!modattr --charid " + CoinCharId + " --pp|" + NewCoins; &nbsp; &nbsp; } &nbsp; &nbsp; sendChat("Change " + CoinP.toUpperCase(), commandvalue); &nbsp; &nbsp; setTimeout(function() { &nbsp; &nbsp; &nbsp; &nbsp; let postSheetValue = getAttrByName(CoinCharId, CoinP, "current"); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;After ChatSetAttr delay:&lt;/b&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;" + escapeHtml(CoinP.toUpperCase()) + " from getAttrByName:&lt;/b&gt; [" + escapeHtml(String(postSheetValue)) + "]" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;/div&gt;" &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; sendChat("My Coins, Altered", "!forselected !mycoins"); &nbsp; &nbsp; }, 700); } function handleDebugCoinAttr(msg) { &nbsp; &nbsp; let args = parseArgs(msg.content); &nbsp; &nbsp; let charID = args.charid || args.characterid || ""; &nbsp; &nbsp; let coin = (args.coin || "").toLowerCase(); &nbsp; &nbsp; if (!charID) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", "&lt;div style='color:red;'&gt;Usage: !debugcoinattr --charid CHARACTER_ID --coin cp&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (!coin) { &nbsp; &nbsp; &nbsp; &nbsp; coin = "cp"; &nbsp; &nbsp; } &nbsp; &nbsp; if (!isValidCoinType(coin)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", "&lt;div style='color:red;'&gt;Invalid coin type: " + escapeHtml(coin) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let character = getObj("character", charID); &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", "&lt;div style='color:red;'&gt;No character found for ID: " + escapeHtml(charID) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; characterid: charID, &nbsp; &nbsp; &nbsp; &nbsp; name: coin &nbsp; &nbsp; }); &nbsp; &nbsp; let sheetValue = getAttrByName(charID, coin, "current"); &nbsp; &nbsp; let output = ""; &nbsp; &nbsp; output += "&lt;div style='border:1px solid #999;padding:6px;background:#fff;'&gt;"; &nbsp; &nbsp; output += "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;"; &nbsp; &nbsp; output += "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(coin.toUpperCase()) + "&lt;br&gt;"; &nbsp; &nbsp; output += "&lt;b&gt;getAttrByName value:&lt;/b&gt; [" + escapeHtml(String(sheetValue)) + "]&lt;br&gt;"; &nbsp; &nbsp; output += "&lt;b&gt;Number of matching attribute objects:&lt;/b&gt; " + attrs.length + "&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; attrs.forEach(function(a, i) { &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;" + escapeHtml(coin.toUpperCase()) + " attribute " + (i + 1) + "&lt;/b&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "id: " + escapeHtml(a.id) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "name: " + escapeHtml(a.get("name")) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "current: [" + escapeHtml(String(a.get("current"))) + "]&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "max: [" + escapeHtml(String(a.get("max"))) + "]&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; }); &nbsp; &nbsp; output += "&lt;/div&gt;"; &nbsp; &nbsp; sendChat("Coin Debug", output); } function handleRepairCoinAttr(msg) { &nbsp; &nbsp; let args = parseArgs(msg.content); &nbsp; &nbsp; let charID = args.charid || args.characterid || ""; &nbsp; &nbsp; let coin = (args.coin || "").toLowerCase(); &nbsp; &nbsp; let valueRaw = args.value; &nbsp; &nbsp; let finalValue = parseInt(valueRaw, 10); &nbsp; &nbsp; if (!charID || !coin || isNaN(finalValue)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Repair", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='color:red;'&gt;Usage: !repaircoinattr --charid CHARACTER_ID --coin cp --value 80&lt;/div&gt;" &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (!isValidCoinType(coin)) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", "&lt;div style='color:red;'&gt;Invalid coin type: " + escapeHtml(coin) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; if (finalValue &lt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", "&lt;div style='color:red;'&gt;Coin value cannot be below 0.&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let character = getObj("character", charID); &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", "&lt;div style='color:red;'&gt;No character found for ID: " + escapeHtml(charID) + "&lt;/div&gt;"); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; characterid: charID, &nbsp; &nbsp; &nbsp; &nbsp; name: coin &nbsp; &nbsp; }); &nbsp; &nbsp; if (!attrs.length) { &nbsp; &nbsp; &nbsp; &nbsp; createObj("attribute", { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charID, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coin, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current: finalValue &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Repair", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Created " + escapeHtml(coin.toUpperCase()) + " for " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(character.get("name")) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " with value " + finalValue + "." + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;/div&gt;" &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; } &nbsp; &nbsp; attrs.forEach(function(a) { &nbsp; &nbsp; &nbsp; &nbsp; a.set("current", finalValue); &nbsp; &nbsp; }); &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; "Coin Repair", &nbsp; &nbsp; &nbsp; &nbsp; "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "Set " + attrs.length + " attribute object(s) named " + &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(coin) + &nbsp; &nbsp; &nbsp; &nbsp; " for " + &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(character.get("name")) + &nbsp; &nbsp; &nbsp; &nbsp; " to " + &nbsp; &nbsp; &nbsp; &nbsp; finalValue + &nbsp; &nbsp; &nbsp; &nbsp; ".&lt;br&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; "Now reopen the sheet and check whether the visible value matches." + &nbsp; &nbsp; &nbsp; &nbsp; "&lt;/div&gt;" &nbsp; &nbsp; ); } function isValidCoinType(coin) { &nbsp; &nbsp; return coin === "cp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coin === "sp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coin === "ep" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coin === "gp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coin === "pp"; } function parseArgs(content) { &nbsp; &nbsp; let parts = content.trim().split(/\s+--/); &nbsp; &nbsp; let result = {}; &nbsp; &nbsp; for (let i = 1; i &lt; parts.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; let part = parts[i]; &nbsp; &nbsp; &nbsp; &nbsp; let firstSpace = part.indexOf(" "); &nbsp; &nbsp; &nbsp; &nbsp; if (firstSpace === -1) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[part.toLowerCase()] = true; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let key = part.substring(0, firstSpace).trim().toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let value = part.substring(firstSpace + 1).trim(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[key] = value; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } &nbsp; &nbsp; return result; } function escapeHtml(text) { &nbsp; &nbsp; if (text === null || text === undefined) { &nbsp; &nbsp; &nbsp; &nbsp; return ""; &nbsp; &nbsp; } &nbsp; &nbsp; return String(text) &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&amp;/g, "&amp;amp;") &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&lt;/g, "&amp;lt;") &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&gt;/g, "&amp;gt;") &nbsp; &nbsp; &nbsp; &nbsp; .replace(/"/g, "&amp;quot;") &nbsp; &nbsp; &nbsp; &nbsp; .replace(/'/g, "&amp;#039;"); }
Let me give this a try!
Tim M said: Let me give this a try! Well, ROTTO, it works well, though for whatever reason (I did not peruse your code deeply) it executes twice.&nbsp; For example, if I add 2 EP to a player who has none, I get 4. That's something I can still run with and I am sure I could figure out. I absolutely love how you make everything a function with a very brief bit of code after the on("chat:message", function(msg). I will probably model this approach for future development. One of my responses about the slowness of my game cane from The Aaron and he suggested I minimize the amount of processing when the game starts up. I think this is the sort of thing that does that? Unfortunately, though, there is still the same weirdness going on with Nigel and his copper pieces. Again appreciate the answer and code so quickly, but I think Nigel is corrupted. Here's your debug message and mine.&nbsp;We are both getting the same attribute ID a weird value for CP.&nbsp; I've got to run one of my parrot buddies to the vet, but after I come back from that and some other stuff, I'm going to poke some more.
So the first thing I would check is whether you still have the original coin script installed somewhere in the Mod/API scripts tab.&nbsp;So my read is: the doubling is probably two active coin scripts, while Nigel’s CP problem is probably a genuinely bad cp&nbsp;attribute object on that character. Those are likely two separate problems. This version adds a simple anti-double-fire guard, cleaner diagnostics, and a direct repair command that can reset Nigel’s cp&nbsp;attribute without using ChatSetAttr. Try this: // Coin Change Script - Full Version // Requires ChatSetAttr for !modattr. // Main command expected: // // !changecoins --NUMBER_TO_ADD_OR_SUBTRACT --CHAR_ID --COIN_TYPE --COIN_NAME --CHARACTER_NAME --CURRENT_COINS // // Examples: // !changecoins --10 --CHARACTER_ID --cp --Copper Pieces --Nigel Krenov --80 // !changecoins ---5 --CHARACTER_ID --gp --Gold Pieces --Nigel Krenov --20 // // Debug: // !debugcoinattr --charid CHARACTER_ID --coin cp // // Repair/sync existing coin attributes: // !repaircoinattr --charid CHARACTER_ID --coin cp --value 80 // // Recreate one coin attribute: // !recreatecoinattr --charid CHARACTER_ID --coin cp --value 80 var CoinChanger1E = CoinChanger1E || (function() { &nbsp; &nbsp; "use strict"; &nbsp; &nbsp; const SCRIPT_NAME = "CoinChanger1E"; &nbsp; &nbsp; const VERSION = "2026-07-07"; &nbsp; &nbsp; const RELAUNCH_DELAY_MS = 800; &nbsp; &nbsp; const DUPLICATE_GUARD_MS = 1500; &nbsp; &nbsp; let recentCommands = {}; &nbsp; &nbsp; on("chat:message", function(msg) { &nbsp; &nbsp; &nbsp; &nbsp; if (msg.type !== "api") return; &nbsp; &nbsp; &nbsp; &nbsp; let rawContent = (msg.content || "").trim(); &nbsp; &nbsp; &nbsp; &nbsp; let lowerContent = rawContent.toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; if (lowerContent.indexOf("!changecoins") === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (isDuplicateCommand(msg)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(SCRIPT_NAME, whisperGM("Duplicate !changecoins command ignored by guard.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handleChangeCoins(msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (lowerContent.indexOf("!debugcoinattr") === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handleDebugCoinAttr(msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (lowerContent.indexOf("!repaircoinattr") === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handleRepairCoinAttr(msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (lowerContent.indexOf("!recreatecoinattr") === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handleRecreateCoinAttr(msg); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (lowerContent.indexOf("!coinchangerversion") === 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat(SCRIPT_NAME, whisperGM("Running " + SCRIPT_NAME + " version " + VERSION)); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); &nbsp; &nbsp; function handleChangeCoins(msg) { &nbsp; &nbsp; &nbsp; &nbsp; let args = msg.content.trim().split(/\s*--/); &nbsp; &nbsp; &nbsp; &nbsp; let newCoinsRaw = args[1]; &nbsp; &nbsp; &nbsp; &nbsp; let coinCharId = (args[2] || "").trim(); &nbsp; &nbsp; &nbsp; &nbsp; let coinType = (args[3] || "").trim().toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; let coinName = (args[4] || "").trim(); &nbsp; &nbsp; &nbsp; &nbsp; let coinCharName = (args[5] || "").trim(); &nbsp; &nbsp; &nbsp; &nbsp; let currentCoinsRaw = args[6]; &nbsp; &nbsp; &nbsp; &nbsp; let newCoins = parseInt(newCoinsRaw, 10); &nbsp; &nbsp; &nbsp; &nbsp; let currentCoins = parseInt(currentCoinsRaw, 10); &nbsp; &nbsp; &nbsp; &nbsp; if (!coinCharId) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", errorBox("No character ID was supplied.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (!coinType) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", errorBox("No coin type was supplied.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (!isValidCoinType(coinType)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", errorBox("Unknown coin type: " + escapeHtml(coinType))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (isNaN(newCoins)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Sorry, but...", errorBox("The coin change must be a number.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (isNaN(currentCoins)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Sorry, but...", errorBox("The current number of " + escapeHtml(coinType.toUpperCase()) + " could not be read correctly.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (currentCoins + newCoins &lt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Sorry, but...", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; errorBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(String(newCoins)) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " would reduce " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(coinCharName) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "'s " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(coinType.toUpperCase()) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " below 0." &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let character = getObj("character", coinCharId); &nbsp; &nbsp; &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Error", errorBox("Could not find character with ID: " + escapeHtml(coinCharId))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: coinCharId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; let sheetValue = getAttrByName(coinCharId, coinType, "current"); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Running:&lt;/b&gt; " + SCRIPT_NAME + " " + VERSION + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(coinType.toUpperCase()) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Sheet value from getAttrByName:&lt;/b&gt; [" + escapeHtml(String(sheetValue)) + "]&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Current coins passed into command:&lt;/b&gt; " + escapeHtml(String(currentCoins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Change requested:&lt;/b&gt; " + escapeHtml(String(newCoins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Expected result:&lt;/b&gt; " + escapeHtml(String(currentCoins + newCoins)) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Matching attribute objects:&lt;/b&gt; " + attrs.length &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; sendCoinAttributeDetails(character, coinCharId, coinType, attrs); &nbsp; &nbsp; &nbsp; &nbsp; let commandValue = "!modattr --charid " + coinCharId + " --" + coinType + "|" + newCoins; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Change " + coinType.toUpperCase(), commandValue); &nbsp; &nbsp; &nbsp; &nbsp; setTimeout(function() { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let postSheetValue = getAttrByName(coinCharId, coinType, "current"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;After ChatSetAttr delay:&lt;/b&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;" + escapeHtml(coinType.toUpperCase()) + " from getAttrByName:&lt;/b&gt; [" + escapeHtml(String(postSheetValue)) + "]" &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("My Coins, Altered", "!forselected !mycoins"); &nbsp; &nbsp; &nbsp; &nbsp; }, RELAUNCH_DELAY_MS); &nbsp; &nbsp; } &nbsp; &nbsp; function handleDebugCoinAttr(msg) { &nbsp; &nbsp; &nbsp; &nbsp; let args = parseArgs(msg.content); &nbsp; &nbsp; &nbsp; &nbsp; let charId = args.charid || args.characterid || ""; &nbsp; &nbsp; &nbsp; &nbsp; let coinType = (args.coin || "cp").toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; if (!charId) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", errorBox("Usage: !debugcoinattr --charid CHARACTER_ID --coin cp")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (!isValidCoinType(coinType)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", errorBox("Invalid coin type: " + escapeHtml(coinType))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let character = getObj("character", charId); &nbsp; &nbsp; &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", errorBox("No character found for ID: " + escapeHtml(charId))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; let sheetValue = getAttrByName(charId, coinType, "current"); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Debug", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Running:&lt;/b&gt; " + SCRIPT_NAME + " " + VERSION + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(coinType.toUpperCase()) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;getAttrByName value:&lt;/b&gt; [" + escapeHtml(String(sheetValue)) + "]&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Matching attribute objects:&lt;/b&gt; " + attrs.length &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; sendCoinAttributeDetails(character, charId, coinType, attrs); &nbsp; &nbsp; } &nbsp; &nbsp; function handleRepairCoinAttr(msg) { &nbsp; &nbsp; &nbsp; &nbsp; let args = parseArgs(msg.content); &nbsp; &nbsp; &nbsp; &nbsp; let charId = args.charid || args.characterid || ""; &nbsp; &nbsp; &nbsp; &nbsp; let coinType = (args.coin || "").toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; let valueRaw = args.value; &nbsp; &nbsp; &nbsp; &nbsp; let finalValue = parseInt(valueRaw, 10); &nbsp; &nbsp; &nbsp; &nbsp; if (!charId || !coinType || isNaN(finalValue)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", errorBox("Usage: !repaircoinattr --charid CHARACTER_ID --coin cp --value 80")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (!isValidCoinType(coinType)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", errorBox("Invalid coin type: " + escapeHtml(coinType))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (finalValue &lt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", errorBox("Coin value cannot be below 0.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let character = getObj("character", charId); &nbsp; &nbsp; &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Repair", errorBox("No character found for ID: " + escapeHtml(charId))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; if (!attrs.length) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; createObj("attribute", { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current: finalValue &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Repair", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Created " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(coinType.toUpperCase()) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " for " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(character.get("name")) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " with value " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(String(finalValue)) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "." &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(function(attr) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr.set("current", finalValue); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Repair", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Set " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attrs.length + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " attribute object(s) named " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(coinType) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " for " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(character.get("name")) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; " to " + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; escapeHtml(String(finalValue)) + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ".&lt;br&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Now reopen the sheet and check whether the visible value matches." &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; } &nbsp; &nbsp; function handleRecreateCoinAttr(msg) { &nbsp; &nbsp; &nbsp; &nbsp; let args = parseArgs(msg.content); &nbsp; &nbsp; &nbsp; &nbsp; let charId = args.charid || args.characterid || ""; &nbsp; &nbsp; &nbsp; &nbsp; let coinType = (args.coin || "").toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; let valueRaw = args.value; &nbsp; &nbsp; &nbsp; &nbsp; let finalValue = parseInt(valueRaw, 10); &nbsp; &nbsp; &nbsp; &nbsp; if (!charId || !coinType || isNaN(finalValue)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Recreate", errorBox("Usage: !recreatecoinattr --charid CHARACTER_ID --coin cp --value 80")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (!isValidCoinType(coinType)) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Recreate", errorBox("Invalid coin type: " + escapeHtml(coinType))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; if (finalValue &lt; 0) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Recreate", errorBox("Coin value cannot be below 0.")); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let character = getObj("character", charId); &nbsp; &nbsp; &nbsp; &nbsp; if (!character) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Recreate", errorBox("No character found for ID: " + escapeHtml(charId))); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; let attrs = findObjs({ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: "attribute", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; let removedCount = attrs.length; &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(function(attr) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attr.remove(); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; let newAttr = createObj("attribute", { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; characterid: charId, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name: coinType, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current: finalValue &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; sendChat( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Coin Recreate", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; infoBox( &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(coinType.toUpperCase()) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Removed old attributes:&lt;/b&gt; " + removedCount + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;Created new attribute ID:&lt;/b&gt; " + escapeHtml(newAttr.id) + "&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "&lt;b&gt;New value:&lt;/b&gt; " + escapeHtml(String(finalValue)) + "&lt;br&gt;&lt;br&gt;" + &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "Close and reopen the character sheet, then test the coin button again." &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ) &nbsp; &nbsp; &nbsp; &nbsp; ); &nbsp; &nbsp; } &nbsp; &nbsp; function sendCoinAttributeDetails(character, charId, coinType, attrs) { &nbsp; &nbsp; &nbsp; &nbsp; let output = ""; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;div style='border:1px solid #999;background:#fff;padding:5px;'&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;Attribute Details&lt;/b&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;Character:&lt;/b&gt; " + escapeHtml(character.get("name")) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;Coin:&lt;/b&gt; " + escapeHtml(coinType.toUpperCase()) + "&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; if (!attrs.length) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;span style='color:#c00;'&gt;&lt;b&gt;No actual attribute object exists for this coin.&lt;/b&gt;&lt;/span&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "The sheet may be showing a default or calculated value.&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; attrs.forEach(function(attr, index) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;b&gt;" + escapeHtml(coinType.toUpperCase()) + " attribute " + (index + 1) + "&lt;/b&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "id: " + escapeHtml(attr.id) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "name: " + escapeHtml(attr.get("name")) + "&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "current: [" + escapeHtml(String(attr.get("current"))) + "]&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; output += "max: [" + escapeHtml(String(attr.get("max"))) + "]&lt;br&gt;&lt;br&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; output += "&lt;/div&gt;"; &nbsp; &nbsp; &nbsp; &nbsp; sendChat("Coin Debug", output); &nbsp; &nbsp; } &nbsp; &nbsp; function isDuplicateCommand(msg) { &nbsp; &nbsp; &nbsp; &nbsp; let key = [ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.playerid || "unknown-player", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.content || "", &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; msg.selected ? JSON.stringify(msg.selected) : "no-selection" &nbsp; &nbsp; &nbsp; &nbsp; ].join("|"); &nbsp; &nbsp; &nbsp; &nbsp; let now = Date.now(); &nbsp; &nbsp; &nbsp; &nbsp; Object.keys(recentCommands).forEach(function(k) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (now - recentCommands[k] &gt; DUPLICATE_GUARD_MS) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; delete recentCommands[k]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; &nbsp; if (recentCommands[key] &amp;&amp; now - recentCommands[key] &lt;= DUPLICATE_GUARD_MS) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return true; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; recentCommands[key] = now; &nbsp; &nbsp; &nbsp; &nbsp; return false; &nbsp; &nbsp; } &nbsp; &nbsp; function isValidCoinType(coinType) { &nbsp; &nbsp; &nbsp; &nbsp; return coinType === "cp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coinType === "sp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coinType === "ep" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coinType === "gp" || &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;coinType === "pp"; &nbsp; &nbsp; } &nbsp; &nbsp; function parseArgs(content) { &nbsp; &nbsp; &nbsp; &nbsp; let parts = content.trim().split(/\s+--/); &nbsp; &nbsp; &nbsp; &nbsp; let result = {}; &nbsp; &nbsp; &nbsp; &nbsp; for (let i = 1; i &lt; parts.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let part = parts[i]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let firstSpace = part.indexOf(" "); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (firstSpace === -1) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[part.toLowerCase()] = true; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let key = part.substring(0, firstSpace).trim().toLowerCase(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let value = part.substring(firstSpace + 1).trim(); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result[key] = value; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; return result; &nbsp; &nbsp; } &nbsp; &nbsp; function escapeHtml(text) { &nbsp; &nbsp; &nbsp; &nbsp; if (text === null || text === undefined) return ""; &nbsp; &nbsp; &nbsp; &nbsp; return String(text) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&amp;/g, "&amp;amp;") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&lt;/g, "&amp;lt;") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/&gt;/g, "&amp;gt;") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/"/g, "&amp;quot;") &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .replace(/'/g, "&amp;#039;"); &nbsp; &nbsp; } &nbsp; &nbsp; function infoBox(content) { &nbsp; &nbsp; &nbsp; &nbsp; return "&lt;div style='border:1px solid #999;background:#fff;padding:6px;'&gt;" + content + "&lt;/div&gt;"; &nbsp; &nbsp; } &nbsp; &nbsp; function errorBox(content) { &nbsp; &nbsp; &nbsp; &nbsp; return "&lt;div style='border:1px solid #c00;background:#fff;padding:6px;color:#c00;'&gt;" + content + "&lt;/div&gt;"; &nbsp; &nbsp; } &nbsp; &nbsp; function whisperGM(content) { &nbsp; &nbsp; &nbsp; &nbsp; return "/w gm " + content; &nbsp; &nbsp; } &nbsp; &nbsp; return { &nbsp; &nbsp; &nbsp; &nbsp; version: VERSION &nbsp; &nbsp; }; }());